## GUI handling
import gedit, gobject, gtk
from gettext import gettext as _

## For Threading and IPC (fork(), pipe(), and Serialization of SharedData)
import cPickle, os, sys

## For Mobwrite Syncing
import random
from mobwrite_lib import MobwriteClient

MOB_SERVER = "http://neil.fraser.name/scripts/q.py"
#http://mobwrite3.appspot.com/scripts/q.py 

## For Debugging
import logging
logging.getLogger().setLevel(logging.DEBUG)

## Because "clientText" is a detached buffer, and as we need a forked process for the 
## sync connection anyway, the diff functions have been moved into thread 2, and thread 1
## simply reads the data via "get_text" and updates clientText on each sync.

## TODO
## * Create a onGeditClose function to display current syncing docs.
## * Merge Neils Mobwrite lib with my sync function.
## * Change gedit tab to show fileId
## * Add something to show the sync status.
## ** CRITICAL: add error handling.
## * Read up on Gedit tab locking

ui_str = """<ui>
  <menubar name="MenuBar">
    <menu name="ToolsMenu" action="Tools">
      <placeholder name="ToolsOps_2">
        <menuitem name="Open Mobwrite Sync" action="mnuOpenDoc"/>
      </placeholder>
    </menu>
  </menubar>
</ui>"""

## ------------------------------ ##    
## A Display box to connect a doc ##
## ------------------------------ ##

class MobwriteClientConnectGui(gtk.Dialog):
  def __init__(self, mobClient):
    logging.debug("Creating GUI config")
    gtk.Dialog.__init__(self, "Connect to..")
    
    tmp = gtk.Table(1,1)
    self.vbox.add(tmp)
    tmp.attach(gtk.Label("Document Id:"),0,1,0,1)
    self.usn = gtk.Entry(max=250)
    self.usn.set_text("demo_editor_text")
    tmp.attach(self.usn, 1,2,0,1)
    self.add_button("Connect", 1)
    tmp.show_all()
    self.usn.grab_focus()

## ---------------- ##
## OnGeditClose Gui ##
## ---------------- ##

class MobwriteClientGeditCloseGui(gtk.Dialog):
  def __init__(self, mobClient):
    gtk.Dialog.__init__(self, "Still in sync..")
   
## -------------- ## 
## The Core Class ##
## -------------- ## 

class MobwriteClientInstance:
  def __init__(self, plugin, window):
    # Init Plugin
    logging.debug("Plugins created for %s" % window)
    ## Set up the plugin, Gedit stuff, GTK stuff, and DMP
    self._window = window
    self._plugin = plugin
    self._insert_ui()
    
    self.mbc = MobwriteClient(MOB_SERVER, "geditClient" + str(random.random()))
    ##self.dmp = dmp_module.diff_match_patch()
    # If a tabs removed, remove it from our synclist.
    self._window.connect("tab-removed", self.tab_removed)
    
    # Start Sync Engine.
    gobject.timeout_add(3000, self.threadedSync)
    logging.debug("Loading Complete")
  
  ## ----------------------------------- ##
  ## --- GUI functions and callbacks --- ##
  ## ----------------------------------- ##
  
  def _insert_ui(self):
    manager = self._window.get_ui_manager()
    
    self._action_group = gtk.ActionGroup("MobClientActions")
    self._action_group.add_actions([("mnuOpenDoc", None, _("Open Mobwrite Document..."), None, _("Open a mobwrite document"), self.mnuOpenDoc)])
    
    manager.insert_action_group(self._action_group, -1)
    self._ui_id = manager.add_ui_from_string(ui_str)
  
  def _remove_ui(self):
    manager = self._window.get_ui_manager()
    manager.remove_ui(self._ui_id)
    manager.remove_action_group(self._action_group)
    manager.ensure_update()
  
  def update_ui(self):
    self._action_group.set_sensitive(self._window.get_active_document() != None)
  
  def mnuOpenDoc(self, action):
    box = MobwriteClientConnectGui(self)
    id = box.run()
    if id == 1:
      u = box.usn.get_text()
      self.attach_tab(None, u)
    box.destroy()

  def attach_tab(self, btn, fileId):
    newtab = self._window.create_tab(True)
    doc = newtab.get_document()
 
    def firstSyncPatch(d, p):
      self.patch_apply_gui(d,p,doc)
      self.mbc.callbacks[fileId]['getText'] = lambda: doc.get_text(*doc.get_bounds())
      self.mbc.callbacks[fileId]['applyPatch'] = lambda d,p: self.patch_apply_gui(d,p,doc)
      self.mbc.callbacks[fileId]['applyRaw'] = lambda x: doc.set_text(x)
                  
    def firstSyncRaw(x):
      doc.set_text(x)
      self.mbc.callbacks[fileId]['getText'] = lambda: doc.get_text(*doc.get_bounds())
      self.mbc.callbacks[fileId]['applyPatch'] = lambda d,p: self.patch_apply_gui(d,p,doc)
      self.mbc.callbacks[fileId]['applyRaw'] = lambda x: doc.set_text(x)
          
    ## Attach to shared list. heres the functions that are needed :)
    self.mbc.shareAdd(fileId, None, firstSyncPatch, firstSyncRaw)
    
    doc.fileId = fileId

    ## TODO make this happen some other way. Info from the server?
    mime = 'text/x-python'
    doc.set_language( gedit.language_manager_get_language_from_mime_type(gedit.get_language_manager(), mime) )

  def tab_removed(self, window, tab):
    doc = tab.get_document()
    if hasattr(doc, 'fileId'):
      print "remove this tab %s" % doc.fileId
      
      txt = self.mbc.callbacks[doc.fileId]['getText']()
      if txt == self.mbc.shares[doc.fileId].shadow:
        print "no changes"
        ## nothing has changed. just remove it.
        self.mbc.shareRemove(doc.fileId)
      else:
        print "we have changes, wait for a sync"
        fid = doc.fileId
        def remove_tab(*args):
          print "removed!"
          self.mbc.shareRemove(fid)
        
        self.mbc.callbacks[doc.fileId]['applyPatch'] = remove_tab
        self.mbc.callbacks[doc.fileId]['applyRaw'] = remove_tab
      del doc.fileId
    
  ## --------------------------------------------------- ## 
  ## --- Syncing functions (threading and callbacks) --- ##
  ## --------------------------------------------------- ##
  
  def threadedSync(self):
    ## run mobwrite.sync inside a thread.
    if not self.mbc.in_sync and len(self.mbc.shares) != 0:
      logging.debug("Starting Threaded Sync...")
      self.mbc.in_sync = True
              
      r, w = os.pipe()
      pid = os.fork()
      ## Run our function
      self.mbc.updateClientTexts() ## lets make sure all clientText's are up-to-date

      if pid == 0:
        # we are the child        
        os.close(r) ## Dont need read.
        w = os.fdopen(w, 'wb')
        build_ret = self.mbc.build_sync(self.mbc.shares)
        status, sync_ret = self.mbc.sync(build_ret)
        cPickle.dump((status, build_ret, sync_ret), w, cPickle.HIGHEST_PROTOCOL)
        w.close()
        sys.exit(0)
        
      # we are the parent
      os.close(w) # We only need the read pipe.
      self.r = os.fdopen(r, 'rb') # turn r into a file object
      gobject.io_add_watch(self.r, gobject.IO_IN | gobject.IO_HUP, self.threadedResult)
    # Return True so we run again.
    return True

  def threadedResult(self, source, condition):
    logging.debug("sync complete")
    (status, obj, res) = cPickle.load(source)
    
    ## only update the doc's that we still have please
    for fileId in self.mbc.shares.keys():
      self.mbc.shares[fileId] = obj[fileId]
      
    ## TODO error handling 
    if status != 0:
      if status == -2:
        ## unable to connect.
        print "Unable to connect to server"
      else:
        print "Unknown error"
      self.mbc.in_sync = False
      return False
            
    ## Deal with the result...
    self.mbc.syncResult(res)
    return False
    
  def patch_apply_gui(self, patches, shadow, doc):
    ## Now walk the array of patches and add to display...
    self.mbc.dmp.patch_splitMax(patches)
    results = []
    delta = 0
    index2 = 0
    pos = 0
    
    doc.begin_not_undoable_action() ## Dont add to edit history.
    
    for patch in patches:
      expected_loc = patch.start2 + delta
      text1 = self.mbc.dmp.diff_text1(patch.diffs)

      ## Ok now use the display as reference
      text = doc.get_text(*doc.get_bounds())
          
      start_loc = self.mbc.dmp.match_main(text, text1, expected_loc)
      if start_loc is None:
        # No match found.  :(
        results.append(False)
      else:
        # Found a match.  :)
        results.append(True)

        delta = start_loc - expected_loc
        text2 = text[start_loc : start_loc + len(text1)]
        diff = self.mbc.dmp.diff_main(text1, text2, False)
        index1 = 0
        for modd in patch.diffs:
          if modd[0] != self.mbc.dmp.DIFF_EQUAL:
            index2 = self.mbc.dmp.diff_xIndex(diff, index1)
            
          if modd[0] == self.mbc.dmp.DIFF_INSERT:
            iters = doc.get_iter_at_offset(start_loc + index2)
            #doc.insert_with_tags_by_name(iters, modd[1], "edit_highlight")
            doc.insert(iters, modd[1])
          elif modd[0] == self.mbc.dmp.DIFF_DELETE:
            iters = doc.get_iter_at_offset(start_loc + index2)
            itere = doc.get_iter_at_offset(start_loc + self.mbc.dmp.diff_xIndex(diff, index1 + len(modd[1])))
            doc.delete(iters, itere)
            
          if modd[0] != self.mbc.dmp.DIFF_DELETE:
            index1 += len(modd[1])
            
    doc.end_not_undoable_action() 
    doc.set_modified(False)

  ## ------------------------------ ##
  ## --- Gedit plugin functions --- ##
  ## ------------------------------ ##
  
  def gedit_close(self):
    ## TODO this function isn't called yet.
    if len(self.mbc.shares) > 0:
      # we still have things to sync.
      print "!"
      ## we need to set all getText callbacks to return there shadow. so we dont keep getting updates. lock docs too?
      ## show dialog with whats happening.
      ## give option to save to file for upload later.
      ## 
             	
  def deactivate(self):
    logging.debug("Plugin stopped for %s" % self._window)
    self._remove_ui()
    self._window = None
    self._plugin = None
    self._action_group = None

## ------------------------------------------------------- ##
## This class is created only once. the root plugin class. ##
## ------------------------------------------------------- ##

class MobwriteClientPlugin(gedit.Plugin):
  def __init__(self):
    gedit.Plugin.__init__(self)
    self._instances = {} # Instances of WindowHelper

  def activate(self, window):
    self._instances[window] = MobwriteClientInstance(self, window)
    window.mobc = self._instances[window]

  def deactivate(self, window):
    self._instances[window].deactivate()
    del self._instances[window]
	  
  def update_ui(self, window):
    self._instances[window].update_ui()
